【Vue指令】解决移动端弹窗“滚动穿透”问题 您所在的位置:网站首页 安卓 对话框 禁止滑动 【Vue指令】解决移动端弹窗“滚动穿透”问题

【Vue指令】解决移动端弹窗“滚动穿透”问题

2023-08-13 13:04| 来源: 网络整理| 查看: 265

一、问题描述 在移动端的H5页面中,我们经常会遇到 点击按钮-->弹窗-->选择选项 这样的场景。而在选项过多出现滚动条时,滚动滚动条至容器的底部或者顶部。再往上或往下拖动滚动条时,滚动动作会出现穿透,这时候底部的body也会一起滚动。 问题总结:内容在滚动到容器的顶部或者底部时,再向上或向下 强行滚动 ,则出现滚动穿透 最佳示例:京东白条弹窗 白条.gif 二、解决方案探索

参考了网上一大堆的解决方法,大可分为三类方法。经过认真的思考和分析,个人的总结如下:

使用js去控制和改变css

弹窗出现 1.1. 记录点击出现弹窗按钮位置的scrollTop 1.2. 给body样式{'overflow': 'hidden'}

弹窗关闭 2.1. 取消body样式{'overflow': 'hidden'} 2.2. 给body样式{'top': scrollTop}

优点:实现简单快捷 缺点:在弹窗一开一关的时间段,如果弹窗不是沾满整个窗口,则会看到body闪烁

使用js去控制弹窗内容区的默认滚动事件

弹窗出现 1.1. 监听内容容器layoutBox的touchstart和touchmove事件 1.2. 监听touchstart事件,得知手指开始滚动内容区的起始位置targetY 1.3. 监听touchmove事件,得知滚动内容区的过程中,变化的位置newTargetY 1.4. 拿到 内容滚动到容器顶部的距离 scrollTop / 内容可滚动的高度 scrollHeight / 当前容器的高度 clientHeight 1.5. 在滚动到顶部和滚动到底部时,阻止内容容器的默认行为。(关键点)

弹窗正常关闭

优点:从出现滚动穿透问题的源头出发,把问题解决,js实现不存在ios兼容问题 缺点:实机验证,个别品牌的机型存在兼容性问题

弹窗内容区禁止滚动,使用js模拟滚动条

弹窗出现 1.1. 监听touchmove事件,全程阻止默认行为 1.2. 监听touchstart和touchmove事件记录手指的移动距离,使用transform: translate3d()属性,实现内容滚动

弹窗正常关闭

优点:js实现不存在ios兼容问题 缺点:ios上丢失了原生滚动条的回弹体验

楼主本次采用的第二种解决方法,那让我们继续往下

三、复习 scrollTop / scrollHeight / clientHeight

首先我们去 mdn 查询这三个属性的定义

scrollTop scrollTop

scrollHeight scrollHeight

clientHeight

clientHeight

三者结合 图片摘自 MDN , 链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/clientHeight https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollHeight https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollTop 侵权则立即删除 复制代码 四、遇到的问题

touchmove无法阻止默认行为,经过在 MDN 查找资料发现:

在移动端,touchmove 无法直接阻止默认行为,原因:使用 passive 改善的滚屏性能

摘要:

image.png

知识点: EventTarget.addEventListener(type, listener, options) options 里的 passive 字段

我们的目的是为了 阻塞 页面的滚动,那将options里的passive设置为false。

五、初步实现

写成一个 mixin

/** * @author cunhang_wei * @description 解决弹窗内容区滚动穿透到body的问题 * @param $refs.layoutBox 需要事先指定 内容容器 */ export default { data () { return { targetY: 0 } }, mounted () { if (this.$refs.layoutBox) { this.$el.addEventListener('touchstart', this.handleTouchstart) this.$el.addEventListener('touchmove', this.handleTouchmove, { passive: false }) } }, methods: { handleTouchstart (e) { this.targetY = Math.floor(e.targetTouches[0].clientY) // 手指起始触摸位置 console.log('handleTouchstart', this.targetY) }, handleTouchmove (e) { let layoutBox = this.$refs.layoutBox // 内容容器 let newTargetY = Math.floor(e.targetTouches[0].clientY) // 手指滑动中触摸位置 let sTop = layoutBox.scrollTop // 内容滚动到容器顶部的高度 let sHeight = layoutBox.scrollHeight // 内容的可滚动高度 let cliHeight = layoutBox.clientHeight // 当前内容容器的高度 if (sTop 0 && e.cancelable) { console.log('下拉到页面顶部') e.preventDefault() } else if (sTop >= sHeight - cliHeight && newTargetY - this.targetY < 0 && e.cancelable) { console.log('上翻到页面底部') e.preventDefault() } } }, beforeDestroy () { if (this.$refs.layoutBox) { this.$el.removeEventListener('touchstart', this.handleTouchstart) this.$el.removeEventListener('touchmove', this.handleTouchmove) } } } 复制代码

写完后发现每一次控制弹窗的滚动穿透,都需要引入这个 mixin 文件,未免有些累赘,查看了 Vue 的官方文档,发现了一种更好的办法,那就是 全局指令

六、写法优化

写成一个全局指令 no-through

/** * @author cunhang_wei * @description 解决弹窗内容区滚动穿透到body的问题(覆盖率90%) * @description 用法 * * * * **/ // 全局变量 targetY var targetY = 0 export default { name: 'no-through', bind: function (el, binding) { // 在binding这个对象里添加属性,是为了在unbind的时候可以取到 binding.handleTouchstart = function (event) { targetY = Math.floor(event.targetTouches[0].clientY) // 手指起始触摸位置 } binding.handleTouchmove = function (event) { let newTargetY = Math.floor(event.targetTouches[0].clientY) // 手指滑动中触摸位置 let sTop = el.scrollTop // 内容滚动到容器顶部的高度 let sHeight = el.scrollHeight // 内容的可滚动高度 let cliHeight = el.clientHeight // 当前内容容器的高度 if (sTop 0 && event.cancelable) { console.log('下拉到页面顶部') event.preventDefault() } else if (sTop >= sHeight - cliHeight && newTargetY - targetY < 0 && event.cancelable) { console.log('上翻到页面底部') event.preventDefault() } } el.addEventListener('touchstart', binding.handleTouchstart) el.addEventListener('touchmove', binding.handleTouchmove, { passive: false }) }, unbind: function (el, binding) { // 重置全局变量 targetY targetY = 0 el.removeEventListener('touchstart', binding.handleTouchstart) el.removeEventListener('touchmove', binding.handleTouchmove, { passive: false }) } } // 最后再去 main.js 注册为全局指令,即可使用。 复制代码 七、实机测试 ios 测试通过 ios13 小米、红米手机 测试通过 安卓 10 一加手机 测试通过 安卓 10 华为手机测试通过 emui11 安卓 10 三星 S8 上存在兼容问题 (初略估计和 Samsung webView 的底层实现有关) 八、总结与思考

解决问题关键

要知道什么情况下才会发生滚动穿透 要知道 scrollTop / scrollHeight / clientHeight 这三个属性,结合它们怎么去判断滚动到了顶部或者底部 H5 移动端touchmove事件默认不支持preventDefault(),需要手动开启 事件捕获和事件冒泡知识: clipboard.png

写法的优化

我们在使用Vue的时候,对于可复用的 js 逻辑,建议写为混入mixins 而对于使用到DOM节点的时候,建议写为指令directives 最后,好好学习不会差!我是爱你们的航少,看完记得点个赞 后续更新(2021/6/25)

最近浏览github,发现有一个更加完美的解决方案:body-scroll-lock,可以很好的解决这个问题

image.png

本地尝试了一下,效果也很不错,大家可以尝试一下



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有